package org.windowkit.toolkit.widget.treetable;

import java.awt.Component;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.util.ArrayList;
import java.util.Collections;
import java.util.EventObject;
import java.util.Hashtable;
import java.util.List;

import javax.swing.AbstractCellEditor;
import javax.swing.DefaultCellEditor;
import javax.swing.DefaultComboBoxModel;
import javax.swing.DefaultListSelectionModel;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.JTree;
import javax.swing.JViewport;
import javax.swing.ListSelectionModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TreeExpansionEvent;
import javax.swing.event.TreeExpansionListener;
import javax.swing.event.TreeWillExpandListener;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;
import javax.swing.table.TableColumnModel;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeSelectionModel;
import javax.swing.tree.ExpandVetoException;
import javax.swing.tree.TreeCellRenderer;
import javax.swing.tree.TreeModel;
import javax.swing.tree.TreePath;

import org.ddevil.data.BasicData;
import org.ddevil.data.BasicDisplayDescriptor;
import org.ddevil.data.Data;
import org.ddevil.data.DisplayDescriptor;
import org.ddevil.data.KeyedChangeTracker.KeyedChange;
import org.ddevil.data.set.DataSet;
import org.ddevil.data.set.SortableDataSet;
import org.ddevil.data.util.DataSetUtils;
import org.windowkit.toolkit.event.EventDispatcher;
import org.windowkit.toolkit.widget.table.render.SortTableHeaderRenderer;
import org.windowkit.toolkit.widget.treetable.DefaultTreeTableModel.DSTreeNode;
import org.windowkit.toolkit.widget.treetable.DefaultTreeTableModel.TreeTableModelStateListener;
import org.windowkit.toolkit.widget.treetable.ui.StretchTreeUI;
import org.windowkit.toolkit.widget.treetable.ui.TreeTableUI;


/**
 * This table uses a tree as its renderer to give the appearance of a tree in
 * tabular format.
 *
 * @author Eric Lundin
 */
@SuppressWarnings("serial")
public class TreeTable extends JTable {
	protected TreeTableCellRenderer treeRenderer;

	TreeTableModelAdapter treeTableModelAdapter;

	private TreeTableCellEditor treeTableCellEditor;

	private final TreeTableModel treeModel;

	Hashtable<String, String> toolTipCache = new Hashtable<String, String>();

	private final TreeTableSelectionFix selectionFix;

	public TreeTable() {
		this(new BasicDisplayDescriptor(new String[] { "FAKE" },
				new String[] { "FAKE" }), new ArrayList<ArrayList<String>>());
	}

	public TreeTable(final DisplayDescriptor labels, final ArrayList<ArrayList<String>> keys) {
		treeModel = new DefaultTreeTableModel(labels, keys);
		// treeModel = new SuperTreeTableModel(labels, keys);
		treeTableCellEditor = new TreeTableCellEditor();
		treeRenderer = new TreeTableCellRenderer(treeModel);
		selectionFix = new TreeTableSelectionFix();
		treeRenderer.addTreeExpansionListener(selectionFix);
		treeRenderer.addTreeWillExpandListener(selectionFix);
		treeTableModelAdapter = new TreeTableModelAdapter(treeModel,
				treeRenderer);

		setModel(treeTableModelAdapter);
		setUI(new TreeTableUI(treeRenderer));
		getTableHeader().setReorderingAllowed(false);
		treeTableCellEditor = new TreeTableCellEditor();

		setDefaultRenderer(TreeTableModel.class, treeRenderer);
		setDefaultEditor(Object.class, treeTableCellEditor);
		addMouseListener(new TableMouseListener());
		setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
		getTableHeader()
				.setDefaultRenderer(
						new SortTableHeaderRenderer(getTableHeader()
								.getDefaultRenderer()));
		setShowGrid(false);
		setIntercellSpacing(new Dimension(0, 0));
		createSynchronizedSelectionModel();
		setColumnSelectionAllowed(false);
		setCellSelectionEnabled(false);
		setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION);
		setRowSelectionAllowed(true);
	}

	public List<KeyedChange> getChanges() {
		return treeModel.getChanges();
	}

	public void setData(final SortableDataSet ds) {
		treeTableModelAdapter.setData(ds);
		treeRenderer.restoreTreeState();
	}

	public void setEditableColumns(final ArrayList<String> editableColumns) {
		treeTableModelAdapter.setEditableColumns(editableColumns);
	}

	/**
	 * Create a new instance of the tree to table selection model class. Set it
	 * as the selection model for both the tree and the table.
	 */
	private void createSynchronizedSelectionModel() {
		ListSelectionWrapper sharedSelectionModel;
		sharedSelectionModel = new ListSelectionWrapper();
		setSelectionModel(sharedSelectionModel.getListSelectionModel());
		treeRenderer.setSelectionModel(sharedSelectionModel);
	}

	/**
	 * Delegate to the tree after updating ourselves.
	 */
	@Override
	public void updateUI() {
		super.updateUI();
		if (treeRenderer != null) {
			treeRenderer.updateUI();
		}
	}

	@Override
	public int getEditingRow() {
		return (getColumnClass(editingColumn) == TreeTableModel.class) ? -1
				: super.getEditingRow();
	}

	/**
	 * Collapse all nodes.
	 */
	public void collapseAll() {
		// TreePath[] paths = treeRenderer.getSelectionPaths();
		for (int i = treeRenderer.getRowCount() - 1; i >= 0;) {
			treeRenderer.collapseRow(i--);
		}
		// if (paths != null) {
		// treeRenderer.setSelectionPaths(paths);
		// }
	}

	/**
	 * Expand all nodes.
	 */
	public void expandAll() {
		// TreePath[] paths = treeRenderer.getSelectionPaths();
		for (int i = 0; i < treeRenderer.getRowCount();) {
			treeRenderer.expandRow(i++);
		}

		// if (paths != null) {
		// treeRenderer.setSelectionPaths(paths);
		// }
	}

	public boolean isDataSelected() {
		return treeRenderer.getSelectionCount() > 0;
	}

	public ArrayList<String> getSelectedRowConnectIds() {
		ArrayList<String> selConIds = null;
		TreePath selectionPath = treeRenderer.getSelectionPath();
		if (selectionPath != null) {
			selConIds = treeModel.getKeysForNode(selectionPath
					.getLastPathComponent());
		}
		return selConIds;
	}

	public DataSet getSelectedRowData() {
		TreePath[] path = treeRenderer.getSelectionPaths();
		DataSet ds = null;
		DataSet temp;
		if (path == null) {
			ds = treeModel.getDataForNode(null);
		} else {
			for (TreePath element : path) {
				temp = treeModel.getDataForNode(element == null ? null
						: element.getLastPathComponent());
				if (temp != null) {
					if (ds == null) {
						ds = temp;
					} else {
						ds.importRecords(temp);
					}
				}
			}
		}
		return ds;
	}

	/**
	 * @param atts
	 */
	public boolean deleteRow(final Data atts, final int level) {
		boolean success;
		treeRenderer.saveTreeState(null, null, level);
		success = treeModel.deleteRow(atts, level);
		treeRenderer.restoreTreeState();
		return success;
	}

	public void deleteRows(final DataSet ds, final int level) {
		treeRenderer.saveTreeState(null, null, level);
		for (int i = 0; i < ds.size(); i++) {
			treeModel.deleteRow(ds.exportRecord(i), level);
		}
		treeRenderer.restoreTreeState();
	}

	/**
	 * @param record
	 */
	public void addRecord(final Data record, final int level) {
		treeRenderer.saveTreeState(null, record, level);
		treeTableModelAdapter.addRecord(record);
		treeRenderer.restoreTreeState();
	}

	/**
	 * We'll cache the last tool tip until data changed is called since java
	 * can't seem to help calling this method constantly, plus the super
	 * implementation uses the same tool tip for every tree node on the given
	 * level based on the tool tip of the cell that was rendered last by each
	 * class of renderer.
	 */
	// public String getToolTipText(MouseEvent event) {
	// Point p = event.getPoint();
	// int rowIndex = rowAtPoint(p);
	// int columnIndex = columnAtPoint(p);
	// System.out.println("getToolTipText");
	// String toolTipText = "";
	// Object currentValue;
	// if (rowIndex != -1 && columnIndex != -1
	// && treeTableModelAdapter != null) {
	// String key = rowIndex + "|" + columnIndex;
	// currentValue = toolTipCache.get(key);
	// if (currentValue == null) {
	// currentValue = treeTableModelAdapter.getValueAt(rowIndex,
	// columnIndex);
	// toolTipText = "<html>"
	// + (currentValue == null ? " " : currentValue)
	// + "</html>";
	// System.out.println("" + currentValue);
	// toolTipCache.put(key, toolTipText);
	// } else {
	// toolTipText = currentValue.toString();
	// }
	// }
	// return toolTipText;
	// }
	@Override
	public void tableChanged(final TableModelEvent e) {
		if (toolTipCache != null) {
			toolTipCache.clear();
		}
		super.tableChanged(e);
	}

	/**
	 *
	 */
	public void clearData() {
		treeTableModelAdapter.clearData();
	}

	/**
	 * @param eventDispatcher
	 */
	public void addTreeSelectionListener(final EventDispatcher eventDispatcher) {
		treeRenderer.getSelectionModel().addTreeSelectionListener(
				eventDispatcher);
	}

	/**
	 *
	 */
	public DisplayDescriptor getDisplayDescriptor() {
		return treeTableModelAdapter.getDisplayDescriptor();
	}

	/**
	 * @param dbId
	 * @param editor
	 */
	public void setColumnEditor(final String dbId, final TableCellEditor editor) {
		int index = treeTableModelAdapter.getIndexOfId(dbId);
		getColumnModel().getColumn(index).setCellEditor(editor);
	}

	@Override
	public void changeSelection(final int rowIndex, final int columnIndex, final boolean toggle,
			final boolean extend) {
		// Don't select cells where nothing is. If nothing is there ignore the
		// selection but still autoscroll.
		if (treeRenderer.isSelectable(rowIndex, columnIndex)) {
			super.changeSelection(rowIndex, columnIndex, toggle, extend);
		} else if (getAutoscrolls()) {
			Rectangle cellRect = getCellRect(rowIndex, columnIndex, false);
			if (cellRect != null) {
				scrollRectToVisible(cellRect);
			}
		}
	}

	/**
	 *
	 */
	public void clearChanges() {
		treeTableModelAdapter.clearChanges();
	}

	public void setAutoClearChangeTracker(final boolean auto) {
		treeTableModelAdapter.setAutoClearChangeTracker(auto);
	}

	@Override
	public int getSelectedRowCount() {
		return super.getSelectedRowCount();
	}

	/**
	 * @param oldValue
	 * @param newValue
	 */
	public boolean modifyRecord(final Data oldValue, final String dbId,
			final Object value, final int level) {
		Data newValue = new BasicData(oldValue);
		newValue.setDataItem(dbId, value);
		treeRenderer.saveTreeState(oldValue, newValue, level);
		boolean success;
		success = treeTableModelAdapter.modifyRecord(oldValue, dbId, value,
				level);
		treeRenderer.restoreTreeState();
		return success;
	}

	/**
	 * @return
	 */
	public ArrayList<Integer> getSelectedLevels() {
		ArrayList<Integer> selLevels = new ArrayList<Integer>();
		TreePath[] selectionPath = treeRenderer.getSelectionPaths();
		int level;
		if (selectionPath != null) {
			for (TreePath element : selectionPath) {
				level = treeModel.getLevel(element
						.getLastPathComponent());
				if (!selLevels.contains(level)) {
					selLevels.add(level);
				}
			}
		}
		return selLevels;
	}

	/**
	 * Get how many items are selected.
	 *
	 * @return
	 */
	public int getSelectedItemCount() {
		return getSelectedRowCount();
	}

	// TODO: I have no clue if this works for all cases. Can probably be a
	// little cleaner.
	public class TreeTableCellEditor extends AbstractCellEditor implements
			TableCellEditor {
		@SuppressWarnings("unused")
		public Component getTableCellEditorComponent(final JTable table,
				final Object value, final boolean isSelected, final int row, final int column) {
			return treeRenderer;
		}

		@Override
		public boolean isCellEditable(final EventObject e) {
			if (e instanceof MouseEvent) {
				for (int counter = getColumnCount() - 1; counter >= 0; counter--) {
					if (getColumnClass(counter) == TreeTableModel.class) {
						MouseEvent me = (MouseEvent) e;
						MouseEvent newME = new MouseEvent(treeRenderer, me
								.getID(), me.getWhen(), me.getModifiers(), me
								.getX()
								- getCellRect(0, counter, true).x, me.getY(),
								me.getClickCount(), me.isPopupTrigger());
						treeRenderer.dispatchEvent(newME);
						break;
					}
				}
			}
			return false;
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see javax.swing.CellEditor#getCellEditorValue()
		 */
		public Object getCellEditorValue() {
			return treeRenderer.getCellEditor().getCellEditorValue();
		}
	}

	/** Swing bug fixes */

	public class TreeTableCellRenderer extends JTree implements
			TableCellRenderer, TreeTableModelStateListener {
		protected int visibleRow;

		int visibleColumn;

		int saveCount = 0;

		int saveLevel;

		ArrayList<TreePath> expandedState;

		ArrayList<Data> keyExpansions = new ArrayList<Data>();

		ArrayList<Data> keySelections = new ArrayList<Data>();

		public TreeTableCellRenderer(final TreeModel model) {
			super(model);
			((DefaultTreeTableModel) model)
					.setTreeTableModelChangeListener(this);
			setRootVisible(false);
			setUI(new StretchTreeUI());
			setInvokesStopCellEditing(true);
			setDragEnabled(false);
		}

		@Override
		public TreeTableModel getModel() {
			return (TreeTableModel) super.getModel();
		}

		@Override
		public void updateUI() {
			// TODO Do we need this?
			super.updateUI();
			TreeCellRenderer tcr = getCellRenderer();
			if (tcr instanceof DefaultTreeCellRenderer) {
				DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer) tcr);
				dtcr.setTextSelectionColor(UIManager
						.getColor("Table.selectionForeground"));
				dtcr.setBackgroundSelectionColor(UIManager
						.getColor("Table.selectionBackground"));
			}
		}

		@Override
		public TreePath getLeadSelectionPath() {
			return getSelectionModel().getLeadSelectionPath();
		}

		@Override
		public int getLeadSelectionRow() {
			return getSelectionModel().getLeadSelectionRow();
		}

		/**
		 * Make the tree's row height the same as the table's row height so
		 * everything fits.
		 */
		@Override
		public void setRowHeight(final int rowHeight) {
			if (rowHeight > 0) {
				super.setRowHeight(rowHeight);
				if (TreeTable.this != null
						&& TreeTable.this.getRowHeight() != rowHeight) {
					TreeTable.this.setRowHeight(getRowHeight());
				}
			}
		}

		@Override
		@SuppressWarnings("unused")
		public void setBounds(final int x, final int y, final int w, final int h) {
			super.setBounds(x, y, /*
									 * TreeTable.this.getColumnModel().getColumn(
									 * visibleColumn).getWidth()
									 */w, this.getHeight());
		}

		protected Object nodeForRow(final int row) {
			TreePath treePath = getPathForRow(row);
			Object node = null;
			if (treePath != null) {
				node = treePath.getLastPathComponent();
			}
			return node;
		}

		@Override
		public void paint(final Graphics g) {
			int y = -visibleRow
					* (getRowHeight() + getIntercellSpacing().height);
			g.translate(0, y);
			super.paint(g);
		}

		Data scrollAtts = null;

		// TODO This is temporary and hack. Will be replaced.
		public void saveTreeState(final Data oldValue,
				final Data newValue, final int level) {
			saveCount++;
			boolean select = true;
			saveLevel = level;
			DefaultTreeTableModel model = ((DefaultTreeTableModel) getModel());
			ArrayList<String> keyList = model.buildKeyListForLevel(level);
			if (saveCount == 1) {
				// If we perform a top level add highlight that value only so
				// the user can find it.
				if (newValue != null && oldValue == null && level == 0) {
					scrollAtts = DataSetUtils.getAttributeSubset(newValue,
							keyList);
					keySelections.add(DataSetUtils.getAttributeSubset(newValue,
							keyList));
					select = false;
				}
				// Otherwise save selections and expansions.
				for (int i = 0; i < getRowCount(); i++) {
					Data key = model.getKeyAttsForNode(nodeForRow(i));
					if (oldValue != null
							&& DataSetUtils
									.attribsEqual(oldValue, key, keyList)) {
						if (isExpanded(i)) {
							keyExpansions.add(DataSetUtils.getAttributeSubset(
									newValue, keyList));
						}
						if (isRowSelected(i) && select) {
							keySelections.add(DataSetUtils.getAttributeSubset(
									newValue, keyList));
						}
					} else {
						if (isExpanded(i)) {
							keyExpansions.add(key);
						}
						if (isRowSelected(i) && select) {
							keySelections.add(key);
						}
					}
				}
			} else {
				// Update the current list of selections and expansions.
				for (int i = 0; i < keyExpansions.size(); i++) {
					if (oldValue != null
							&& DataSetUtils.attribsEqual(oldValue,
									keyExpansions.get(i), keyList)) {
						keyExpansions.set(i, DataSetUtils.getAttributeSubset(
								newValue, keyList));
					}
				}
				for (int i = 0; i < keySelections.size(); i++) {
					if (oldValue != null
							&& DataSetUtils.attribsEqual(oldValue,
									keySelections.get(i), keyList)) {
						keySelections.set(i, DataSetUtils.getAttributeSubset(
								newValue, keyList));
					}
				}
			}
		}

		public void restoreTreeState() {
			SwingUtilities.invokeLater(new Runnable() {
				public void run() {
					// We only want to restore our state once all operations
					// have completed.
					if (--saveCount > 0) {
						return;
					}
					DefaultTreeTableModel model = ((DefaultTreeTableModel) getModel());
					TreePath scrollPath = null;
					Data current;
					int row = -1;
					ArrayList<TreePath> selectPaths = new ArrayList<TreePath>();
					for (int i = 0; i < getRowCount(); i++) {
						current = ((DefaultTreeTableModel) getModel())
								.getKeyAttsForNode(nodeForRow(i));
						ArrayList<String> keyList = model
								.buildKeyListForLevel(saveLevel);
						for (int j = 0; j < keyExpansions.size(); j++) {
							if (DataSetUtils.attribsEqual(keyExpansions.get(j),
									current, keyList)) {
								expandRow(i);
								break;
							}
						}
						for (int j = 0; j < keySelections.size(); j++) {
							if (DataSetUtils.attribsEqual(keySelections.get(j),
									current, keyList)) {
								selectPaths.add(getPathForRow(i));
								if (keySelections.size() == 1
										&& scrollAtts == null) {
									scrollAtts = current;
								}
								break;
							}
						}
						if (scrollAtts != null) {
							if (DataSetUtils.attribsEqual(current, scrollAtts,
									keyList)) {
								scrollPath = getPathForRow(i);
								row = i;
								selectPaths.add(scrollPath);
							}
						}
					}
					if (selectPaths.size() > 0) {
						setSelectionPaths(selectPaths.toArray(new TreePath[0]));
					}

					// Here we want to scroll so the item is visible but only if
					// it isn't visible now. This way already visible items
					// don't 'jump' when we scroll.
					// We will also try to put the scrolled item at the top of
					// the viewport instead of the bottom.
					if (row != -1) {
						Rectangle r = TreeTable.this.getCellRect(row, 0, true);
						JViewport viewport = ((JViewport) TreeTable.this
								.getParent());
						if (viewport != null) {
							boolean shouldScroll = !viewport.getViewRect()
									.intersects(r);
							if (shouldScroll) {
								viewport.setViewPosition(new Point(viewport
										.getViewPosition().x, r.y));
							}
						}
					}
					scrollAtts = null;
					keySelections = new ArrayList<Data>();
					keyExpansions = new ArrayList<Data>();
					saveCount = 0;
				}
			});
		}

		public boolean isSelectable(final int row, final int column) {
			boolean isSelectableCell;
			DSTreeNode node = (DSTreeNode) nodeForRow(row);
			isSelectableCell = false;
			if (getModel().getChildCount(node) != 0) {
				DSTreeNode child = (DSTreeNode) getModel().getChild(node, 0);
				if (node.getDataLevel() <= column
						&& child.getDataLevel() > column) {
					isSelectableCell = true;
				}
			} else if (node.getDataLevel() <= column) {
				isSelectableCell = true;
			}
			return isSelectableCell;
		}

		/**
		 * If this isn't the first instance of the node having data in the row
		 * render with the table's default renderer. Otherwise use the tree.
		 */
		public Component getTableCellRendererComponent(final JTable table,
				final Object value, final boolean isSelected, final boolean hasFocus, final int row,
				final int column) {
			int level;
			DSTreeNode node = ((DSTreeNode) nodeForRow(row));
			level = node.getDataLevel();

			if (level != column) {
				Class< ? extends Object> cellClass = value == null ? String.class
						: value.getClass();
				JComponent c = (JComponent) table.getDefaultRenderer(cellClass)
						.getTableCellRendererComponent(table, value,
								isSelected && isSelectable(row, column),
								hasFocus, row, column);
				return c;
			}
			JComponent c = (JComponent) getCellRenderer()
					.getTreeCellRendererComponent(this, node, isSelected,
							isExpanded(row), node.isLeaf(), row, hasFocus);
			c.setBounds(this.getBounds());
			if (isExpanded(row)) {
				((JLabel) c).setIcon(UIManager.getIcon("Tree.openIcon"));
			}
			if (isSelected && isSelectable(row, column)) {
				c.setBackground(table.getSelectionBackground());
			} else {
				c.setBackground(table.getBackground());
			}

			visibleRow = row;
			visibleColumn = column;
			return c;
		}
	}

	/**
	 * This class acts as a delegate between the tree's selection model and the
	 * table's selection model, allowing them to be synchronized with each
	 * other.
	 *
	 * @author Eric Lundin
	 */
	class ListSelectionWrapper extends DefaultTreeSelectionModel {
		protected boolean updatingListSelectionModel;

		public ListSelectionWrapper() {
			super();
			listSelectionModel = new DefaultListSelectionModel() {
				int selectionLevel;

				/**
				 * Listeners are notified in reverse order of being added when
				 * there is a change.This is super hack but we need the first
				 * listener which we'll assume is the tree selection model which
				 * contains this class to be notified first so it can update the
				 * tree portion of the selection model. Otherwise the tree
				 * selection data will not be synchronized with the table
				 * selection data.
				 *
				 * @param l
				 */
				@Override
				public void addListSelectionListener(final ListSelectionListener l) {
					if (listenerList
							.getListenerCount(ListSelectionListener.class) > 0) {
						ListSelectionListener lsl = listenerList
								.getListeners(ListSelectionListener.class)[0];
						listenerList.remove(ListSelectionListener.class, lsl);
						listenerList.add(ListSelectionListener.class, l);
						listenerList.add(ListSelectionListener.class, lsl);
					} else {
						listenerList.add(ListSelectionListener.class, l);
					}
				}

				@Override
				public void addSelectionInterval(final int index0, final int index1) {
					int min = Math.min(index0, index1);
					int max = Math.max(index0, index1);
					TreeTableModel m = treeRenderer.getModel();
					int anchor = -1;
					if (isSelectionEmpty()) {
						Object node = null;
						treeRenderer.nodeForRow(index0);
						if (node != null) {
							selectionLevel = m.getLevel(node);
						} else {
							return;
						}
					}
					if (min == -1 || max == -1) {
						return;
					}
					ArrayList<Integer> indices = new ArrayList<Integer>();
					for (int i = min; i <= max; i++) {
						if (isValidSelectableNode(i)) {
							indices.add(i);
						}
					}
					if (index1 < index0) {
						Collections.reverse(indices);
					}

					ArrayList<int[]> intervals = buildIntervals(indices);
					if (intervals.size() != 0) {
						for (int i = 0; i < intervals.size(); i++) {
							if (anchor == -1) {
								anchor = intervals.get(i)[0];
							}
							super.addSelectionInterval(intervals.get(i)[0],
									intervals.get(i)[1]);
						}
						if (intervals.size() > 1) {
							setAnchorSelectionIndex(anchor);
						}
					}
				}

				private ArrayList<int[]> buildIntervals(
						final ArrayList<Integer> indices) {
					ArrayList<int[]> intervals = new ArrayList<int[]>();
					int start;
					int min;
					int max;
					if (indices.size() != 0) {
						if (indices.size() == 1) {
							intervals.add(new int[] { indices.get(0),
									indices.get(0) });
						} else {
							start = indices.get(0);
							for (int i = 0; i < indices.size(); i++) {
								if (i == indices.size() - 1) {
									intervals.add(new int[] { start,
											indices.get(i) });
								} else {
									int currentIndex = indices.get(i)
											.intValue();
									int nextIndex = indices.get(i + 1)
											.intValue();
									max = Math.max(currentIndex, nextIndex);
									min = Math.min(currentIndex, nextIndex);
									if (min + 1 != max) {
										intervals.add(new int[] { start,
												indices.get(i) });
										start = nextIndex;
									}
								}
							}
						}
					}
					return intervals;
				}

				// @Override
				// TODO This method will be used for add once the model is no
				// longer based off a sorted dataset.
				// public void insertIndexInterval(
				// int index, int length,
				// boolean before) {
				// // super.insertIndexInterval(index, length, before);
				// }

				@Override
				public void setSelectionInterval(final int index0, final int index1) {
					int min = Math.min(index0, index1);
					int max = Math.max(index0, index1);
					TreeTableModel m = treeRenderer.getModel();
					if (min == -1 || max == -1) {
						return;
					}
					selectionLevel = m
							.getLevel(treeRenderer.nodeForRow(index0));
					ArrayList<Integer> indices = new ArrayList<Integer>();
					for (int i = min; i <= max; i++) {
						if (isValidSelectableNode(i)) {
							indices.add(i);
						}
					}
					if (index1 < index0) {
						Collections.reverse(indices);
					}

					ArrayList<int[]> intervals = buildIntervals(indices);
					if (intervals.size() != 0) {
						for (int i = 0; i < intervals.size(); i++) {
							if (i == 0) {
								super.setSelectionInterval(intervals.get(i)[0],
										intervals.get(i)[1]);
							} else {
								super.addSelectionInterval(intervals.get(i)[0],
										intervals.get(i)[1]);
							}
						}
						if (intervals.size() > 1) {
							setAnchorSelectionIndex(intervals.get(0)[0]);
						}
					}
				}

				@Override
				public void setLeadSelectionIndex(final int index) {
					if (index != -1 && !isValidSelectableNode(index)) {
						return;
					}
					super.setLeadSelectionIndex(index);
				}

				@Override
				public void setAnchorSelectionIndex(final int index) {
					if (index != -1 && !isValidSelectableNode(index)) {
						return;
					}
					super.setAnchorSelectionIndex(index);
				}

				private boolean isValidSelectableNode(final int i) {
					TreeTableModel m = treeRenderer.getModel();
					return m.getLevel(treeRenderer.nodeForRow(i)) == selectionLevel;
				}

				@Override
				public boolean isSelectedIndex(final int i) {
					return ((i < getMinSelectionIndex()) || (i > getMaxSelectionIndex())) ? false
							: super.isSelectedIndex(i)
									&& isValidSelectableNode(i);
				}

			};
			getListSelectionModel().addListSelectionListener(
					new ListSelectionHandler());
		}

		ListSelectionModel getListSelectionModel() {
			return listSelectionModel;
		}

		@Override
		public void resetRowSelection() {
			if (!updatingListSelectionModel) {
				updatingListSelectionModel = true;
				try {
					super.resetRowSelection();
				} finally {
					updatingListSelectionModel = false;
				}
			}
		}

		@Override
		public void addSelectionPath(final TreePath path) {
			addSelectionPaths(new TreePath[] { path });
		}

		//
		@Override
		public void addSelectionPaths(final TreePath[] paths) {
			if (paths != null) {
				ArrayList<int[]> intervals = buildIntervals(paths);
				for (int i = 0; i < intervals.size(); i++) {
					listSelectionModel.addSelectionInterval(
							intervals.get(i)[0], intervals.get(i)[1]);
				}
			}
		}

		//
		@Override
		public void setSelectionPath(final TreePath path) {
			setSelectionPaths(new TreePath[] { path });
		}

		//
		@Override
		public void setSelectionPaths(final TreePath[] pPaths) {
			if (pPaths == null) {
				listSelectionModel.clearSelection();
			} else {
				ArrayList<int[]> intervals = buildIntervals(pPaths);
				for (int i = 0; i < intervals.size(); i++) {
					if (i == 0) {
						listSelectionModel.setSelectionInterval(intervals
								.get(i)[0], intervals.get(i)[1]);
					} else {
						listSelectionModel.addSelectionInterval(intervals
								.get(i)[0], intervals.get(i)[1]);
					}
				}
			}
		}

		private ArrayList<int[]> buildIntervals(final TreePath[] paths) {
			ArrayList<int[]> intervals = new ArrayList<int[]>();
			ArrayList<Integer> indices = new ArrayList<Integer>();
			int start;
			int min;
			int max;
			for (TreePath path : paths) {
				if (path != null) {
					indices.add(treeRenderer.getRowForPath(path));
				}
				if (indices.get(indices.size() - 1) < indices.get(0)) {
					Collections.reverse(indices);
				}
			}
			if (indices.size() != 0) {
				if (indices.size() == 1) {
					intervals.add(new int[] { indices.get(0), indices.get(0) });
				} else {
					start = indices.get(0);
					for (int i = 0; i < indices.size(); i++) {
						if (i == indices.size() - 1) {
							intervals.add(new int[] { start, indices.get(i) });
						} else {
							int currentIndex = indices.get(i).intValue();
							int nextIndex = indices.get(i + 1).intValue();
							max = Math.max(currentIndex, nextIndex);
							min = Math.min(currentIndex, nextIndex);
							if (min + 1 != max) {
								intervals
										.add(new int[] { start, indices.get(i) });
								start = nextIndex;
							}
						}
					}
				}
			}
			return intervals;
		}

		/**
		 * If <code>updatingListSelectionModel</code> is false, this will
		 * reset the selected paths from the selected rows in the list selection
		 * model.
		 */
		protected void updateSelectedPathsFromSelectedRows() {
			if (!updatingListSelectionModel) {
				updatingListSelectionModel = true;
				try {
					int min = listSelectionModel.getMinSelectionIndex();
					int max = listSelectionModel.getMaxSelectionIndex();

					clearSelection();
					if (min != -1 && max != -1) {
						ArrayList<TreePath> paths = new ArrayList<TreePath>();
						for (int counter = min; counter <= max; counter++) {
							if (listSelectionModel.isSelectedIndex(counter)) {
								TreePath selPath = treeRenderer
										.getPathForRow(counter);
								if (selPath != null) {
									paths.add(selPath);
								}
							}
						}
						super.setSelectionPaths(paths.toArray(new TreePath[0]));
					}
				} finally {
					updatingListSelectionModel = false;
				}
			}
		}

		class ListSelectionHandler implements ListSelectionListener {
			@SuppressWarnings("unused")
			public void valueChanged(final ListSelectionEvent e) {
				// System.out.println("Value changed is asjusting: "
				// + e.getValueIsAdjusting() + " first "
				// + e.getFirstIndex() + " last " + e.getLastIndex());
				// if (!e.getValueIsAdjusting()) {
				updateSelectedPathsFromSelectedRows();
				// }
			}
		}
	}

	/**
	 * This class dispatches mouse events to the tree and translates the
	 * coordinates to the given tree node's coordinate system. This would not be
	 * necessary if the tree was not split over multiple columns.
	 *
	 * @author Eric Lundin
	 */
	protected class TableMouseListener implements MouseListener {
		public void mouseClicked(final MouseEvent me) {
			dispatchEventToTree(me);
		}

		public void mouseEntered(final MouseEvent me) {
			dispatchEventToTree(me);
		}

		public void mouseExited(final MouseEvent me) {
			dispatchEventToTree(me);
		}

		public void mousePressed(final MouseEvent me) {
			dispatchEventToTree(me);
		}

		public void mouseReleased(final MouseEvent me) {
			dispatchEventToTree(me);
		}

		private void dispatchEventToTree(final MouseEvent me) {
			if (treeRenderer != null) {
				int row = rowAtPoint(me.getPoint());
				int column = columnAtPoint(me.getPoint());
				if (row != -1 && column != -1
						&& treeRenderer.isSelectable(row, column)
						&& me.getClickCount() == 2) {
					MouseEvent newME = new MouseEvent(treeRenderer, me.getID(),
							me.getWhen(), me.getModifiers(), me.getX(), me
									.getY(), me.getClickCount(), me
									.isPopupTrigger(), me.getButton());
					treeRenderer.dispatchEvent(newME);
				}
			}
		}
	}

	/**
	 * Bug Fix for Bug 4292511. This code is needed to prevent the first column
	 * header to determine the height of the entire table header. <B>Remove this
	 * code when bug is fixed.</B>
	 *
	 * @see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4292511
	 */
	@Override
	public JTableHeader createDefaultTableHeader() {
		// TODO: Remove method when bug 4292511 is fixed.
		return new JTableHeaderFix(columnModel);
	}

	public void resetHeaderHeight() {
		// TODO: Remove method and calls to method in TablePane when bug
		// 4292511 is fixed.
		// TODO WE might not need this here.
		((JTableHeaderFix) getTableHeader()).resetHeight();
	}

	@SuppressWarnings("serial")
	/**
	 * This fix creteates the preferred height for the table based on the
	 * tallest header label. JTableHeader normally creates it off the first.
	 */
	protected class JTableHeaderFix extends JTableHeader {
		// TODO: Remove class when bug 4292511 is fixed.
		private int preferrredHeight = -1;

		public JTableHeaderFix(final TableColumnModel columnModel) {
			super(columnModel);
		}

		private Component getHeaderRenderer(final int columnIndex) {
			TableColumn aColumn = getColumnModel().getColumn(columnIndex);
			TableCellRenderer renderer = aColumn.getHeaderRenderer();
			if (renderer == null) {
				renderer = getDefaultRenderer();
			}
			return renderer.getTableCellRendererComponent(getTable(), aColumn
					.getHeaderValue(), false, false, -1, columnIndex);
		}

		private int getPreferredHeight() {
			if (preferrredHeight == -1) {
				preferrredHeight = 0;
				int columnCount = getColumnModel().getColumnCount();
				for (int column = 0; column < columnCount; column++) {
					Component comp = getHeaderRenderer(column);
					int rendererHeight = comp.getPreferredSize().height;
					preferrredHeight = Math.max(preferrredHeight,
							rendererHeight);
				}
			}
			return preferrredHeight;
		}

		@Override
		public Dimension getPreferredSize() {
			return new Dimension(super.getPreferredSize().width,
					getPreferredHeight());
		}

		@Override
		public void columnAdded(final TableColumnModelEvent e) {
			preferrredHeight = -1;
			super.columnAdded(e);
		}

		@Override
		public void columnRemoved(final TableColumnModelEvent e) {
			preferrredHeight = -1;
			super.columnRemoved(e);
		}

		public void resetHeight() {
			preferrredHeight = -1;
		}
	}

	/**
	 * This class will hopefully restore selection after a tree has expanded
	 * since the table's fire data changed is nice enough to clear out selection
	 * for us.
	 *
	 * @author Eric Lundin
	 */
	protected class TreeTableSelectionFix implements TreeWillExpandListener,
			TreeExpansionListener {
		TreePath[] selectionPaths;

		/*
		 * (non-Javadoc)
		 *
		 * @see javax.swing.event.TreeWillExpandListener#treeWillCollapse(javax.swing.event.TreeExpansionEvent)
		 */
		public void treeWillCollapse(final TreeExpansionEvent event)
				throws ExpandVetoException {
			stopEditor(event);

			selectionPaths = treeRenderer.getSelectionPaths();
		}

		/**
		 * Check if we are editing. If we are stop. We don't want to expand the
		 * tree while the table is editing as it will put the new value into the
		 * old cell where the value used to be. This isn't good.
		 *
		 * @param event
		 */
		private void stopEditor(final TreeExpansionEvent event) {
			if (isEditing()) {
				getCellEditor().stopCellEditing();
			}
			((JTree) event.getSource()).stopEditing();
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see javax.swing.event.TreeWillExpandListener#treeWillExpand(javax.swing.event.TreeExpansionEvent)
		 */
		public void treeWillExpand(final TreeExpansionEvent event)
				throws ExpandVetoException {
			stopEditor(event);
			((JTree) event.getSource()).stopEditing();
			selectionPaths = treeRenderer.getSelectionPaths();
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see javax.swing.event.TreeExpansionListener#treeCollapsed(javax.swing.event.TreeExpansionEvent)
		 */
		@SuppressWarnings("unused")
		public void treeCollapsed(final TreeExpansionEvent event) {
			restoreSelectionState();
		}

		/*
		 * (non-Javadoc)
		 *
		 * @see javax.swing.event.TreeExpansionListener#treeExpanded(javax.swing.event.TreeExpansionEvent)
		 */
		@SuppressWarnings("unused")
		public void treeExpanded(final TreeExpansionEvent event) {
			restoreSelectionState();
		}

		private void restoreSelectionState() {
			treeRenderer.setSelectionPaths(selectionPaths);
		}

	}

	/**
	 * @param exportAttribs
	 */
	public void setChangeExportKeys(final ArrayList<ArrayList<String>> exportAttribs) {
		treeTableModelAdapter.setChangeExportKeys(exportAttribs);
	}

	public TableCellEditor createUniqueComboRenderer(final String dbId,
			final ArrayList<String> keys, final ArrayList<String> items, final int level) {
		return new UniqueComboRenderer(dbId, keys, items, level);
	}

	public class UniqueComboRenderer extends DefaultCellEditor {

		private final ArrayList<String> keys;

		private final String myId;

		private final int myLevel;

		private final ArrayList<String> validItems;

		public UniqueComboRenderer(final String dbId, final ArrayList<String> key,
				final ArrayList<String> items, final int level) {
			super(new JComboBox());
			myId = dbId;
			keys = key;
			validItems = items;
			myLevel = level;
		}

		@Override
		public Component getTableCellEditorComponent(final JTable table,
				final Object value, final boolean isSelected, final int row, final int column) {
			JComboBox combo = (JComboBox) super.getTableCellEditorComponent(
					table, value, isSelected, row, column);
			TreeTableModel m = treeRenderer.getModel();
			ArrayList<String> currentItems = new ArrayList<String>(validItems);
			Object editNode = treeRenderer.nodeForRow(row);
			for (int i = 0; i < treeRenderer.getRowCount(); i++) {
				Object currentNode = treeRenderer.nodeForRow(i);
				if (i != row && m.getLevel(currentNode) == myLevel) {
					if (DataSetUtils.attribsEqual(m.getDataForNode(currentNode)
							.exportRecord(0), m.getDataForNode(editNode)
							.exportRecord(0), keys)) {
						currentItems.remove(m.getDataForNode(currentNode)
								.exportRecord(0).getValue(myId));
					}
				}
			}
			combo.setModel(new DefaultComboBoxModel(currentItems.toArray()));
			combo.setSelectedItem(value);
			return combo;
		}
	}
}
